Now that we can enjoy our sphere from all angles, it is time to customize the way it is rendered. This means writing our own shader to alter the rendering directly on GPU.
As we are using a DirectX renderer, we will write it in HLSL. If you are unfamiliar with this language, it is advised to check some documentation about it to fully understand what we will be doing here. For instance, you can find some good knowledge here or there.
Within nkGraphics, the class symbolizing a shader program is called Program. Let's start manipulating it, by first including :
Having those includes will allow to alter the code to use a Program. Let's put this code right after we create the entity and populate it with the mesh :
A Program is a resource like any other, and as such we use its dedicated manager to get one. Our local program will be named "program"... How original.
The most important part of a Program are the sources. They go into a ProgramSourcesHolder structure which can be submitted to the Program for compilation.
As we do a simple shader supposed to paint a mesh, we will need the vertex and pixel stages.
We won't go over the details of the HLSL code in this tutorial, as its only aim is to transform the mesh onto the screen and apply a constant colour.
However, an important point to notice is the fact that the entry point methods should be called main. Those are used by the program compilation within the component.
Back to C++, we specify the program should load from the memory we just populated, and request a load operation, as usual with resources in the component.
Now, notice the vertex stage input :
Part of this input is coming from the mesh that will get drawn using the shader program.
As mentioned in tutorial N°03, it is important to have the right semantic names for our attributes, found in the mesh layouts.
First, POSITION is expected from the mesh, and implies that there is an attribute named like this in the mesh layout.
Last attribute has the name WORLD_MATRIX, and is tied to the Shader's instance slots we will discover later.
They also link their data to a semantic name it is important to refer to.
If the combination of a Mesh and a Shader fails in feeding what a Program requires, it won't be possible to link them together.
However, as mentioned just now, we need a way to provide input a mesh won't cover to the program we just created. This is where we properly introduce the Shader class.
A Shader represents an association between a Program and an input. It is possible to have one Program linked to many Shaders, each with different input to feed it.
Now that we know the next step we need, let's see the required includes :
And use them right away :
Our shader is now created. Let's see what we can do with it :
We start by attaching its program right away; the one we just created. Next, we register a ConstantBuffer inside, which is precisely what the Program needs.
The ConstantBuffer is formed by ShaderPassMemorySlots. Basically, a pass slot is fed once per pass, right before rendering it. Slots offer many capabilities, and amongst them is feeding a constant buffer with the view and projection matrices.
This relates directly to the constant buffer given within the program, so the order of declaration is important. First comes the view matrix, then comes the projection matrix, like we declared them in HLSL.
Next is the creation of a ShaderInstanceMemorySlot. This type of slot is specific to the rendering of a render queue. Basically, it is triggered once per instance of mesh that needs to be rendered. This type of slot maps directly to an attribute within the mesh, and is tied to it through its semantic name. By setting it as a world matrix, we will get a matrix with semantic name WORLD_MATRIX, representing the transformation matrix for a given instance of a mesh. This transformation comes from the Node attached to the Entity containing the mesh.
To summarize, within HLSL, we have the vertex position, and the world matrix. The vertex position is given by the mesh itself. However, the world matrix depends on the Entity, and the Node it is attached to, changing its position, orientation, or scale within the virtual world.
This specific data is given through the slot meant for instances. By specifying we want it to feed the world matrix, we ensure it will do so for each instance in our rendering.
Final step is to load the shader, like we always do with resources we use. Now the shader is ready to be used during rendering, and to do so, we need to alter the Entity once more :
Let's see right away what we will get when launching the program !
This covers the way to create and use a shader within the component. For now, it is pretty simple, but in the next steps we will see how to spice it a bit.